package com.cbs.android.component.network;

import android.content.Context;
import android.os.Handler;
import android.os.Message;

import com.cbs.android.component.log.L;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;

/**
 * Created by brochexu on 9/2/14.
 */
public class HttpClient {
    private static final String TAG = HttpClient.class.getName();
    private static final int CONNECTTIMEOUT = 5000;
    private static final int READTIMEOUT = 10000;
    private static final int WRITETIMEOUT = 10000;

    private OkHttpClient client = null;
    private static final MyHandler handler = new MyHandler();
    private boolean debug = false;

    public static class Builder {
        private OkHttpClient.Builder builder;

        public Builder() {
            builder = new OkHttpClient.Builder()
                    .connectTimeout(CONNECTTIMEOUT, TimeUnit.MILLISECONDS)
                    .readTimeout(READTIMEOUT, TimeUnit.MILLISECONDS)
                    .writeTimeout(WRITETIMEOUT, TimeUnit.MILLISECONDS)
                    .cookieJar(new CookieJar() {
                        private final HashMap<String, List<Cookie>> cookieStore = new HashMap<>();

                        @Override
                        public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                            cookieStore.put(url.host(), cookies);
                        }

                        @Override
                        public List<Cookie> loadForRequest(HttpUrl url) {
                            List<Cookie> cookies = cookieStore.get(url.host());
                            return cookies != null ? cookies : new ArrayList<Cookie>();
                        }
                    });
        }

        public Builder connectTimeout(long timeout, TimeUnit timeUnit) {
            builder.connectTimeout(timeout, timeUnit);
            return this;
        }

        public Builder readTimeout(long timeout, TimeUnit timeUnit) {
            builder.readTimeout(timeout, timeUnit);
            return this;
        }

        public Builder writeTimeout(long timeout, TimeUnit timeUnit) {
            builder.writeTimeout(timeout, timeUnit);
            return this;
        }

        public Builder cookieJar(CookieJar cookieJar) {
            builder.cookieJar(cookieJar);
            return this;
        }

        public Builder cache(Context context, String cachePath) {
            File rootFile = context.getCacheDir();
            File cacheFile = new File(rootFile.getPath() + "/" + cachePath);
            if (!cacheFile.exists()) {
                boolean b = cacheFile.mkdirs();
                if (!b) {
                    L.w(TAG, "cache create failed, no cache used");
                    return this;
                }
            }
            Cache cache = new Cache(cacheFile, 20 * 1024 * 1024);
            builder.cache(cache);
            return this;
        }

        public Builder cache(File cacheFile) {
            if (!cacheFile.exists()) {
                boolean b = cacheFile.mkdirs();
                if (!b) {
                    L.w(TAG, "cache create failed, no cache used");
                    return this;
                }
            }
            Cache cache = new Cache(cacheFile, 20 * 1024 * 1024);
            builder.cache(cache);
            return this;
        }

        public Builder ssl(InputStream cert) {
            try {
                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                Certificate certificate = certificateFactory.generateCertificate(cert);

                String keyStoreType = KeyStore.getDefaultType();
                KeyStore keyStore = KeyStore.getInstance(keyStoreType);
                keyStore.load(null, null);
                keyStore.setCertificateEntry("ca", certificate);

                String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm);
                trustManagerFactory.init(keyStore);

                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(null, trustManagerFactory.getTrustManagers(), null);

                builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0]);
            } catch (Exception e) {
                L.e(TAG, "", e);
            } finally {
                try {
                    cert.close();
                } catch (IOException e) {
                    L.e(TAG, "", e);
                }
            }
            return this;
        }

        public Builder ssl(Context context, int cerResId) {
            InputStream cert = context.getResources().openRawResource(cerResId);
            return ssl(cert);
        }

        public Builder followRedirects(boolean followRedirects) {
            builder.followRedirects(followRedirects);
            return this;
        }

        public HttpClient build() {
            return new HttpClient(this);
        }
    }

    private HttpClient(Builder builder) {
        client = builder.builder.build();
    }

    private HttpClient(HttpClient httpClient) {
        client = httpClient.client.newBuilder().build();
        debug = httpClient.debug;
    }

    public OkHttpClient getOkHttpClient() {
        return client;
    }

    public void send(Request request) {
        okhttp3.Request rawRequest = request.build();
        if (rawRequest == null) {
            if (debug) {
                L.w(TAG, "request terminated");
            }
            return;
        }
        printRequest(rawRequest);

        Call call = client.newCall(rawRequest);
        call.enqueue(new MyCallback(request));
    }

    public Response sendSync(Request request) throws IOException {
        okhttp3.Request rawRequest = request.build();

        if (rawRequest == null) {
            if (debug) {
                L.w(TAG, "request terminated");
            }
            return null;
        }

        printRequest(rawRequest);

        Call call = client.newCall(rawRequest);
        okhttp3.Response rawResponse = call.execute();
        Response response = new Response(rawResponse, request);

        printResponse(response);

        return response;
    }

    public Response sendForCache(Request request) {
        Request cacheRequest = new Request(request).cacheControl(CacheControl.FORCE_CACHE);
        Response response = null;
        try {
            response = sendSync(cacheRequest);
        } catch (IOException e) {
            L.w(TAG, "", e);
        }
        return response;
    }


    public void clearCache() {
        Cache cache = client.cache();
        if (cache != null) {
            File cacheFile = cache.directory();
            try {
                cache.delete();
            } catch (IOException e) {
                L.w(TAG, "", e);
            }
            cache = new Cache(cacheFile, 1024 * 100);
            // TODO 这样做是否合理
            client = client.newBuilder().cache(cache).build();
        }
    }

    public long getCacheSize() {
        Cache cache = client.cache();
        if (cache != null) {
            try {
                return cache.size();
            } catch (IOException e) {
                L.e(TAG, "", e);
                return 0;
            }
        } else {
            return 0;
        }
    }

    public long getCacheMaxSize() {
        Cache cache = client.cache();
        if (cache != null) {
            return cache.maxSize();
        } else {
            return 0;
        }
    }

    /**
     * Default connect timeout (in milliseconds).
     */
    public final int getConnectTimeout() {
        return client.connectTimeoutMillis();
    }

    /**
     * Default read timeout (in milliseconds).
     */
    public final int readTimeoutMillis() {
        return client.readTimeoutMillis();
    }

    /**
     * Default write timeout (in milliseconds).
     */
    public final int writeTimeoutMillis() {
        return client.writeTimeoutMillis();
    }

    public boolean followRedirects() {
        return client.followRedirects();
    }


    public CookieJar cookieJar() {
        return client.cookieJar();
    }

    public boolean isDebug() {
        return debug;
    }

    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Returns a shallow copy of this HttpClient.
     */
    @Override
    public HttpClient clone() {
        return new HttpClient(this);
    }

    private String getHeaderString(Headers headers) {
        StringBuilder result = new StringBuilder();
        int i = 0;
        for (int size = headers.size(); i < size; ++i) {
            result.append(headers.name(i)).append(": ").append(headers.value(i)).append("\t");
        }
        return result.toString();
    }

    private void printRequest(okhttp3.Request rawRequest) {
        if (debug) {
            L.ws(TAG);
            L.w(TAG, String.format("request: (%s) %s", rawRequest.method(), rawRequest.url().toString()));
            L.w(TAG, "header: " + getHeaderString(rawRequest.headers()));
            if (rawRequest.body() != null) {
                if (FormBody.class.isAssignableFrom(rawRequest.body().getClass())) {
                    FormBody formBody = (FormBody) rawRequest.body();
                    StringBuilder content = new StringBuilder();
                    content.append("body: ");
                    content.append("ContentType: " + formBody.contentType());
                    content.append("\t");
                    content.append("ContentLength: " + formBody.contentLength());
                    content.append("\n");
                    for (int i = 0; i < formBody.size(); i++) {
                        content.append(formBody.encodedName(i));
                        content.append("=");
                        content.append(formBody.encodedValue(i));
                        content.append("&");
                    }
                    L.w(TAG, content.toString());
                }
            }
        }
    }

    private void printResponse(Response response) {
        if (debug) {
            okhttp3.Response rawResponse = response.getRawResponse();
            L.ws(TAG);
            L.w(TAG, String.format("response: %d (%s) %s", rawResponse.code(), rawResponse.request().method(), rawResponse.request().url().toString()));
            L.w(TAG, String.format("header: %s", getHeaderString(rawResponse.headers())));
            if (response.getResult() != null) {
                String desc = response.desc();
                if (desc == null || desc.equals("")) {
                    desc = "(CBSNetwork: no message)";
                }
                L.w(TAG, String.format("result: %s", desc));
            }
        }
    }

    private class MyCallback implements Callback {
        private Request request = null;

        public MyCallback(Request request) {
            this.request = request;
        }

        @Override
        public void onFailure(Call call, IOException e) {
            okhttp3.Request rawRequest = call.request();
            if (debug) {
                L.ws(TAG);
                L.w(TAG, String.format("response: error (%s) %s", rawRequest.method(), rawRequest.url().toString()));
            }
            Object[] objs = new Object[2];
            objs[0] = request;
            objs[1] = e;
            handler.obtainMessage(0, objs).sendToTarget();
        }

        @Override
        public void onResponse(Call call, okhttp3.Response rawResponse) throws IOException {
            Response response = new Response(rawResponse, request);

            printResponse(response);

            handler.obtainMessage(1, response).sendToTarget();
        }
    }

    private static class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            int action = msg.what;
            if (action == 1) {
                Response response = (Response) msg.obj;
                Request request = response.getRequest();
                if (request.getResponseHandler() != null) {
                    request.getResponseHandler().onResponse(response);
                }
            } else {
                Object[] objs = (Object[]) msg.obj;
                Request request = (Request) objs[0];
                Exception exception = (Exception) objs[1];
                if (request.getResponseHandler() != null) {
                    request.getResponseHandler().onException(request, exception);
                }
            }
        }
    }
}
